웹 사이트 보안에 대해 알아보자
모던 리액트 공부
2024-02-27
프론트엔드에서 해야 할 일이 많아질수록 프론트엔드 코드의 규모 역시 증가하고, 코드의 규모가 증가한다는 점은 보안 취약점에 노출될 가능성이 커진다는 것을 의미한다.
보안이슈는 프레임워크나 라이브러리가 100% 해결해주는 것이 아니기 때문에 개발자 스스로 주의가 필요하다.
XSS
크로스 사이트 스크립팅(Cross-site-Scripting,XSS)이란 웹 애플리케이션에서 가장 많이 보이는 취약점 중 하나이다. 웹 사이트 개발자가 아닌 제 3자가 웹 페이지에 악성 스크립트를 삽입해 실행할 수 있는 취약점을 의미한다.
이 취약점은 일반적으로 게시판과 같이 글을 입력할 수 있는 사이트에서 발생한다.
예를 들어 어떤 사용자가 다음의 글을 올린다.
<p>사용자가 글을 작성했습니다.</p>
<script>
alert('xss')
</script>
만약 위 글을 방문했을 때 아무런 조치가 없다면 script가 실행되어 window.alert도 함께 실행된다. 이 script가 실행된다면 웹 사이트 개발자가 할 수 있는 모든 작업을 함께 수행할 수 있으며 쿠키를 획득하거나 로그인 정보를 탈취하는 등의 작업을 할 수 있다.
그럼 리액트에서 이 XSS 이슈를 어떻게 막을 수 있을까?
dangerouslySetInnerHTML prop
dangerouslySetInnerHTML은 특정 브라우저 DOM의 innerHTML을 특정 내용으로 교체하는 것이다. 일반적으로 게시판 등과 같이 사용자가 입력한 내용을 브라우저에 표시하는 내용으로 사용된다.
function App(){
//결과물은 <div>First . Second이다</div>
return <div danerouslySetInnerHTML = {{
__html : 'First · Second'
}}></div>
}
danerouslySetInnerHTML은 오직 __html을 키로 갖고 있는 객체만 인수로 받을 수 있으며, 이 인수로 넘겨받은 문자열을 DOM에 표시하는 역할을 한다.
dangerouslySetInnerHTML과 비슷한 방법으로 DOM에 직접 내용을 삽입하는 방법으로 useRef가 있다. useRef를 사용하면 직접 DOM에 접근할 수 있다.
const html = `<span><svg/onload=alert(origin)></span>`
function App(){
const divRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if(divRef.current){
divRef.current.innerHTML = html
}
})
return <div ref = {divRef}></div>
}
리액트에서 XSS 공격을 피해보자.
리액트에서 XSS이슈를 피하는 가장 확실한 방법은 제 3자가 삽입할 수 있는 HTML을 안전한 HTML로 한번 치환하는 것이다. 이러한 과정을 새니타이즈 또는 이스케이프라고 하는데, 가장 확실한 방법은 npm에 있는 라이브러를 사용하는 것이다.
- DOMpurity
- sanitize-html
- js-xss
sanitize-html을 사용한 예시를 살펴보자.
import React from 'react';
import sanitizeHtml from 'sanitize-html';
function MyComponent(props) {
// 허용하는 태그와 속성을 정의합니다.
const allowedTags = ['div', 'p', 'span', 'h1', 'h2'];
const allowedAttributes = {
'a': ['href', 'name', 'target'],
'img': ['src']
};
// props로 받은 HTML을 새니타이즈합니다.
const sanitizedHtml = sanitizeHtml(props.html, {
allowedTags: allowedTags,
allowedAttributes: allowedAttributes,
});
// 새니타이즈된 HTML을 렌더링합니다.
// 이때, React에서 HTML을 직접 렌더링하기 위해선 dangerouslySetInnerHTML을 사용해야 합니다.
return <div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />;
}
단순히 콘텐츠를 보여줄 떄 뿐만 아니라 사용자가 콘텐츠를 저장할 때도 한번 이스케이프 과정을 거치는 것이 더 효율적이고 안전하다. 애초에 XSS위험이 있는 콘텐츠를 저장하는 것이 예기치 못한 문제를 발생시킬 수 있고, 한번 이스케이프 하면 그 뒤로 일일이 이스케이프 과정을 안거쳐도 된다.
예를 들어 POST요청으로 입력받은 HTML을 클라이언트에서만 이스케이프 과정을 거친다고 해보자. 일반적인 사용에서는 크게 문제가 되지 않지만, 스크립트나 curl 명령어로 POST요청을 날리는 경우 서버에서 이스케이프를 처리하는 것이 훨씬 안전하다.
마지막으로 게시판이 웹 사이트에 없더라도 XSS는 충분히 발생할 수 있다. 따라서 개발자는 자신이 작성한 코드가 아닌 query,GET 파라미터 등 모든 코드를 위험한 코드로 간주하고 적절히 처리하는 게 좋다.
getServerSideProps와 서버 컴포넌트를 주의하자
서버 사이드 렌더링과 서버 컴포넌트는 성능 이점을 줌과 동시에 서버라는 개발 환경을 프론트엔드 개발자에게 쥐어준 셈이다. 서버에는 일반 사용자에게 노출이 되면 안되는 정보들이 담겨있기 때문에 브라우저에 정보를 쥐어줄 떄 조심해야 한다.
export default function App(({cookie} : {cookie:string})) {
if(!validateCookie(cookie)){
Router.replace()
return null
}
}
export const getServerSideProps = async(ctx: GetServerSidePropsContext) => {
const cookie = ctx.req.headers.cookie || ''
return {
props{
cookie
}
}
}
위 예제에서는 getServerSideProps로 cookie를 가져온 후 클라이언트 컴포넌트에 제공해 클라이언트에서 쿠키의 유효성에 따라 작업을 진행한다. 이는 보안 관점에서는 좋지 않다. getServerSideProps가 반환하는 props는 모두 사용자의 HTML에 기록되고 보안 위협에 노출되는 값이 된다.
또한 getServerSideProps에서 처리할 수 있는 리다이렉트가 클라이언트에서 실행되기 때문에 성능적인 측면에서 문제의 소지가 있다. 따라서 getServerSideProps가 반환하는 props 값은 , 서버 컴포넌트가 반환하는 props는 반드시 필요한 값으로만 철저히 제한되어야 한다.
export default function App({token} : {token : string}) {
const user = JSON.parse(window.abot(token.split('.')[1]))
const userId = user.id
}
export const getServerSideProps = async(ctx:GetServerSidePropsContext) => {
const cookie = ctx.req.headers.cookie || ''
const token = validateCookie(cookie)
if(!token){
return {
redirect: {
destination:'/404',
permanent : false
}
}
}
return {
props:{
token
}
}
}
a 태그의 값에 적절한 제한을 주자.
웹 개발 시 a태그의 href로 javascript코드를 넣어둘 수 있다. 이는 주로 a태그의 기본 기능, href로 선언된 URL로 페이지 이동을 막고, onClick 이벤트와 같이 이벤트 핸들러만 작동하기 위한 용도로 주로 사용된다.
function App(){
function handleClick(){
}
return <a href = "javascript:;" onClick = {handleClick}>링크</a>
}
이러한 방법은 마크업 관점에서 안티패턴이라고 볼 수 있다. a 태그는 반드시 페이지 이동이 있을 때만 사용하는 것이 좋다. 페이지 이동 없이 이벤트 핸들러만 작동시키고 싶다면 a보다는 button을 사용하는 것이 좋다.
HTTP의 보안 헤더
HTTP의 보안 헤더란 브라우저가 렌더링하는 내용과 관련된 보안 취약점을 미연에 방지하기 위해 브라우저와 함께 작동하는 헤더를 뜻힌다. 이는 브라우저 보안에 가장 기초적인 부분으로 HTTP 보안 헤더만 효율적으로 사용할 수 있어도 많은 보안 취약점을 방지할 수 있다.
HTTP의 strict-transport-security 응답 헤더는 모든 사이트가 HTTPS를 통해 접근해야 하며 만약 HTTP로 접근하는 경우 이런 모든 시도는 HTTPS로 변경하게 된다.
Strict-Transport-Security : max-age=<expire-time>; includeSubDomains
expire time설정은 브라우저가 기억해야 하는 시간을 의미하며, 초 단위로 기록된다. 이 기간 내에 HTTP로 사용자가 요청한다 하더라도, 브라우저는 이 시간을 기억하고 있다가 자동으로 HTTPS로 요청하게 된다. 만약 헤더의 시간이 경과하면 HTTPS로 로드를 시도한 다음 응답에 따라 HTTPS로 이동하는 등의 작업을 수행한다.
만약 0으로 되어 있다면 헤더가 즉시 만료되고 HTTP로 요청하게 된다. 일반적으로 1년 단위로 허용한다. includeSubDomains가 있을 경우 이런 규칙이 모든 하위 도메인에도 적용된다.
X-XSS-Protection
이 기술은 사파리와 구형 브라우저에서만 제공되는 기능이다.
이 헤더는 페이지에서 XSS 취약점이 발견되면 페이지 로딩을 중단하는 헤더이다. 만약 HTTP 헤더에 Content-Security-Policy가 있다면 그닥 필요 없지만 Content-Security-Policy를 지원하지 않는 구형 브라우저에서 사용이 가능하다. 그러나 이 헤더를 전적으로 믿어선 안되며, 반드시 페이지 내부에서 XSS 처리를 하는 것이 좋다.
X-XSS-Protection: 0
X-XSS-Protection: 1
X-XSS-Protection: 1; mode = block
X-XSS-Protection: 1; report = <reporting-url>
- 0은 기본적으로 XSS 필터링을 끈다.
- 1은 기본값으로 XSS 필터링을 키게 된다. 만약 XSS 공격이 페이지 내부에서 감지되면 XSS 코드를 제거한 안전한 페이지를 보여준다.
- 1; mode = block은 1과 유사하지만 코드를 제거하는 것이 아닌, 접근 자체를 막는다.
- 1; report =
X-Frame-Options
X-Frame-Options은 페이지를 frame,iframe,embed,object 내부에서 렌더링을 허용할지를 나타낼 수 있다. 예를 들어 네이버와 비슷한 주소를 가진 페이지가 있고 이 페이지에서 네이버를 iframe으로 렌더링한다. 사용자는 이 페이지를 진짜 네이버로 오해할 수 있고, 공격자는 이를 활용해 사용자의 개인정보를 탈취할 수 있다.
X-Frame-Options은 외부에서 자신의 페이지를 위와 같은 방식으로 삽입되는 것을 막아준다.
X-Frame-Options : DENY
X-Frame-Options : SAMEORIGIN
- DENY : 만약 위와 같은 프레임 관련 코드가 있다면 무조건 막는다.
- SAMEORIGIN : 같은 origin에 대해서만 프레임을 허용한다.
Permissions-Policy
Permissions-policy는 웹사이트에서 사용할 수 있는 기능과 사용할 수 없는 기능을 명시적으로 선언하는 헤더이다. 다양한 브라우저의 기능이나 API를 선택적으로 활성화하거나 필요에 따라 비활성화 할 수 있다. (geolocation 등)
# 모든 geolocation 사용을 막는다.
Permissions-Policy : geolocation=()
# gelolcation을 페이지 자신과 몇 가지 페이지에 대해서만 허용
Permissions-Policy : geolocation=(self `https://~`)
# 카메라는 모든 곳에서 허용한다.
Permission-Policy : camera=*;
X-Content-Type-Options
먼저 MIME이 무엇인지 알아야한다. MIME이란 Multipurpose Internet Mail Extension의 약자로 Content-type의 값으로 사용된다. 이름에서처럼 원래는 메일을 전송할 때 사용하던 방식으로 현재는 Content-type에서 대표적으로 사용된다.
여기서 X-Content-Type-Options이란 Content-type 헤더에서 제공하는 MIME 유형이 브라우저에 의해 임의로 변경되지 않게 하는 헤더이다.
즉 Content-type : text/css 헤더가 없는 파일은 브라우저가 임의로 CSS로 사용할 수 없으며, Content-type : text/javascript나 Content-type : application/javascript 헤더가 없는 파일은 자바스크립트로 해석할 수 없다.
예를 들어 어떤 공격자가 .jpg 파일을 웹 서버에 업로드 했는데 실제 그 파일은 그림이 아닌 자바스크립트 정보를 담고 있다. 브라우저는 .jpg로 파일을 요청했지만 실제 스크립트가 담기고, 보안 위험에 노출된다.
다음과 같이 헤더를 설정하면 파일의 타입이 CSS나 MIME이 text/css가 아닌 경우, 파일 내용이 script나 MIME 타입이 자바스크립트 타입이 아니면 차단한다.
X-Content-Type-Options : nosniff
Referrer-Policy
HTTP 요청에는 Referer라는 헤더가 존재하고, 이 헤더는 현재 요청을 보낸 페이지의 주소가 담기게 된다. 먼저 출처와 이를 구성하는 용어에 대해 알아보자.
https://yceffort.kr의 경우 다음과 같이 구성되어 있다.
- scheme : HTTPS 프로토콜을 의미한다.
- hostname : yceffort.kr이라는 호스팅명을 의미한다.
- port : 443 포트를 의미한다. (보통 HTTPS의 경우 443포트를 사용)
웹 사이트 보안의 기본적인 대전제는 동일 출처 Same-Origin 이다. 이것을 Same-Origin Policy(SOP) 라고 한다.
어떤 요청이 동일한 출처에서 발생하지 않은 경우에는 Cross-Site 또는 Cross-Origin 이라고 하며, 개인 정보 보호 및 웹 공격 방어 차원에서 특정한 기능이나 정보가 제한된다.
origin(출처)이란 scheme + hostname + port 의 조합이다. 예를 들어, URL이 https://www.example.com:443/search?query=frontend인 경우, origin은 https://www.example.com:443이 된다.
Referer의 정확한 정의는, '현재 요청을 보낸 페이지의 절대 혹은 부분 주소' 이다. 아래와 같은 경우에 존재한다.
- 사용자의 링크 클릭
- 이미지, 스크립트, iframe, 기타 리소스 등 브라우저의 하위 리소스(subresource) 요청
위처럼 사이트에 방문한 사용자가 어디에서 왔고 누구인지를 식별할 수 있게 될 수 있습니다. 요컨대 잠재적인 취약점이 될 수 있다.
정책으로는 다음의 것들이 존재한다.
-
no-referrer : Referer 헤더를 전혀 보내지 않습니다.
-
no-referrer-when-downgrade : 보안 연결(HTTPS)에서 비보안 연결(HTTP)로 이동할 때 Referer 헤더를 보내지 않습니다. 이는 기본적인 정책입니다.
-
same-origin : 같은 출처에서 요청한 경우에만 Referer 헤더를 보냅니다.
-
origin : Referer 헤더에 원본 URL의 출처(즉, 프로토콜, 호스트, 포트)만 포함시킵니다.
-
strict-origin : 보안 연결에서 비보안 연결로 이동할 때 Referer 헤더를 보내지 않는 것을 제외하고는 origin과 같습니다.
-
origin-when-cross-origin : 같은 출처에서 요청한 경우 전체 URL을, 그렇지 않은 경우 원본 URL의 출처만 Referer 헤더에 포함시킵니다.
-
strict-origin-when-cross-origin : 보안 연결에서 비보안 연결로 이동할 때 Referer 헤더를 보내지 않는 것을 제외하고는 origin-when-cross-origin과 같습니다.
-
unsafe-url : Referer 헤더에 전체 원본 URL을 포함시킵니다. 이 옵션은 개인 정보가 노출될 위험이 있으므로 사용에 주의해야 합니다.
Content-Security-Policy
콘텐츠 보안 정책은 XSS 공격이나 데이터 삽입 공격과 같은 보안 위험을 막기 위해 설계되었다.
-src
font-src,img-src 등 다양한 src를 제어할 수 있는 제어문이다. 예를 들어 font-src는 다음과 같이 쓸 수 있다.
Content-Security-Policy : font-src <source>
이렇게 선언하면 font의 src로 가져오는 소스를 제한할 수 있다. 여기에 선언된 font 소스만 가져올 수 있다.
Next에서 HTTP 경로별로 보안 헤더를 설정할 수 있다. 이 설정은 next.config.js에서 추가할 수 있따.
const Headers = [
{
key:'key',
value:'value'
}
]
module.exports = {
async headers(){
return [
{
source:'/:path*',
headers:Headers
}
]
}
}
추가할 수 있는 것은 다음과 같다.
-
X-XSS-Protection
-
X-Frame-Options
-
Permissions-Policy
-
X-Content-Type-Options
-
Referer-Policy
-
Content-Security-Policy